JavaScript application frameworks: the parts

JavaScript application frameworks: the parts

Nuts and bolts of JS application frameworks: how to write tools for messaging, extending objects, and changing state; how to safely integrate DOM or utility libraries and plugins; how to integrate CSS and templates; how to connect all the pieces; how to apply that knowledge to choosing an existing framework.

76f795cabbf80024b1024517c67f0bcf?s=128

Garann Means

May 29, 2013
Tweet

Transcript

  1. JavaScript Application Frameworks the parts | Garann Means @garannm

  2. in applications and frameworks, the code is the easy part

    the hard part is decision making
  3. and it is hard to build an app figuring out

    what it should do translating requirements to workflows making it all work together (even when you have good tools)
  4. any framework may need customization a framework is a basis

    your app may have slightly different needs different ways of handling the same concept may exist within the same app it may have no opinion on other concepts
  5. compare to jQuery: tons of functionality some core, some more

    esoteric discovering new features leads to better ways of doing things the most discoverable features are those we know should be there
  6. using a tool well means understanding the decisions it makes,

    and making good decisions about how to use it
  7. what should be there and what we should expect from

    it
  8. we expect framework methods to live on a prototype unlike

    a library, whose methods should usually not require an instance allows creation of objects set up to work together instantiation lets us keep related functionality together
  9. but we also need methods for the app containing the

    objects global initialization event broadcasting global state changes stateless utilities
  10. so it makes sense to start with a global (

    f u n c t i o n ( F r m w r k ) { f u n c t i o n _ i n i t ( ) { } F r m w r k . t e s t = f u n c t i o n ( ) { c o n s o l e . l o g ( " y u p " ) ; } ; _ i n i t ( ) ; } ) ( w i n d o w . F r m w r k = { } ) ; F r m w r k . t e s t ( ) ; / / y u p
  11. so thatG s nice. but it should do something

  12. we were going to instantiate some objects state data or

    both we don't want to have to create too many types
  13. one object we know we need f u n c

    t i o n _ i n i t ( ) { F r m w r k . a p p = n e w F r m w r k . O b j e c t ( ) ; }
  14. so letG s make that the default F r m

    w r k . O b j e c t = f u n c t i o n ( o b j ) { $ . e x t e n d ( t r u e , t h i s , { s e l : " b o d y " } , o b j ) ; i f ( t h i s . s e l ) { t h i s . e l = $ ( t h i s . s e l ) ; } r e t u r n t h i s ; } ;
  15. hold up.. whose $ is that

  16. utility libraries it's also good to understand libraries but this

    is not a talk on libraries make a smart choice providing the most you need but no more jQuery, Underscore, Zepto, etc. focus on connecting pieces, not writing essential tools
  17. including third-party libraries or tools can be appended to your

    core file other users don't need to import the additional script difficult to change or upgrade may conflict with other versions in use
  18. leaving it up to implementers other users need to import

    the script separately, in order framework should work with all supported versions may make sense to allow uses of library to be overriden
  19. a note about dependencies

  20. in this example, jQuery is used like so: ( f

    u n c t i o n ( F r m w r k , $ ) { . . . } ) ( w i n d o w . F r m w r k = { } , w i n d o w . j Q u e r y ) ;
  21. for a more modular approach, you might use require.js d

    e f i n e ( [ " j Q u e r y " ] , f u n c t i o n ( $ ) { . . . } ) ;
  22. letG s get back to our base object F r

    m w r k . O b j e c t = f u n c t i o n ( o b j ) { $ . e x t e n d ( t r u e , t h i s , { s e l : " b o d y " } , o b j ) ; i f ( t h i s . s e l ) { t h i s . e l = $ ( t h i s . s e l ) ; } r e t u r n t h i s ; } ;
  23. what do we expect from a data object a place

    to store data the ability to save and update itself notifications if it changes
  24. setting and changing data ideally, set on object creation but

    may need to be set on an empty object (like the app) should be possible to update one or more properties
  25. the data should be its own object F r m

    w r k . O b j e c t = f u n c t i o n ( o b j ) { $ . e x t e n d ( t r u e , t h i s , { s e l : " b o d y " , d a t a : { } } , o b j ) ; i f ( t h i s . s e l ) { t h i s . e l = $ ( t h i s . s e l ) ; } r e t u r n t h i s ; } ;
  26. we want control over how thatG s updated F r

    m w r k . O b j e c t . p r o t o t y p e . s e t = _ s e t = f u n c t i o n ( d a t a , v a l ) { i f ( t y p e o f d a t a = = " s t r i n g " ) { t h i s . d a t a [ d a t a ] = v a l ; } e l s e { f o r ( k e y i n d a t a ) { t h i s . d a t a [ k e y ] = d a t a [ k e y ] ; } } r e t u r n t h i s ; } ;
  27. and a shortcut for the app itself F r m

    w r k . s e t = f u n c t i o n ( ) { _ s e t . a p p l y ( F r m w r k . a p p , a r g u m e n t s ) ; } ;
  28. why not just have the implementer modify ÇobjÓ.data directly this

    is a cheap workaround to observing the objects can fire off events, update server allows for a predictable pattern allows us to do a little magic
  29. the downside is it means we also need a way

    to get data F r m w r k . O b j e c t . p r o t o t y p e . g e t = _ g e t = f u n c t i o n ( k e y ) { r e t u r n t h i s . d a t a [ k e y ] ; } ; F r m w r k . g e t = f u n c t i o n ( ) { r e t u r n _ g e t . a p p l y ( F r m w r k . a p p , a r g u m e n t s ) ; } ;
  30. we could also condense the two, jQuery- style F r

    m w r k . O b j e c t . p r o t o t y p e . d a t a = _ d a t a = f u n c t i o n ( d a t a , v a l ) { i f ( a r g u m e n t s . l e n g t h = = 1 & & t y p e o f d a t a = = " s t r i n g " ) { r e t u r n t h i s . d a t a [ k e y ] ; } e l s e { . . . } ;
  31. once we have data, we have something to store data

    persistence tends to be boring to write we need several types of XHRs we want saving and updating to be as invisible as possible
  32. should we assume REST depends very much on your backend

    may be more JavaScripty to do CRUD from all endpoints: data with no ID: create ID only: read data and ID: update ID and string or number: delete but only if that fits the larger architecture
  33. letG s assume it does F r m w r

    k . O b j e c t = f u n c t i o n ( o b j ) { $ . e x t e n d ( t r u e , t h i s , { s e l : " b o d y " , d a t a : { } , e n d p o i n t : n u l l } , o b j ) ; i f ( t h i s . s e l ) { t h i s . e l = $ ( t h i s . s e l ) ; } r e t u r n t h i s ; } ;
  34. we want to sync whenever an update happens F r

    m w r k . O b j e c t . p r o t o t y p e . s e t = _ s e t = f u n c t i o n ( d a t a , v a l ) { i f ( t y p e o f d a t a = = " s t r i n g " ) { t h i s . d a t a [ d a t a ] = v a l ; } e l s e { f o r ( k e y i n d a t a ) { t h i s . d a t a [ k e y ] = d a t a [ k e y ] ; } } i f ( t h i s . e n d p o i n t ) { t h i s . s y n c ( ) ; } r e t u r n t h i s ; } ;
  35. additionally, we want explicit methods to delete or read an

    object please use your imagination for those Nremember that thing about CRUD being boring Ñ
  36. then, of course, we need a function to do the

    work F r m w r k . O b j e c t . p r o t o t y p e . s y n c = f u n c t i o n ( d a t a , c m d ) { v a r i d = t y p e o f d a t a = = " n u m b e r " ? d a t a : u n d e f i n e d , t h a t = t h i s ; $ . p o s t ( t h i s . e n d p o i n t , { i d : d a t a . i d | | i d , c o m m a n d : c m d , d a t a : i d ? u n d e f i n e d : d a t a } , f u n c t i o n ( r e s ) { i f ( t y p e o f r e s = = " n u m b e r " ) { t h a t . d a t a . i d = r e s ; } e l s e i f ( r e s . i d ) { f o r ( k e y i n r e s ) { t h a t . d a t a [ k e y ] = d a t a [ k e y ] ; } }
  37. several decisions reflected there not RESTful, as discussed flexible signature

    for a single method without explicit flags most important: happens in the background
  38. weG ve been talking about objects but we want those

    objects to talk to a framework
  39. it would be nice to have notifications when object data

    is set when the data is synced with the server if an error occurs anywhere along the way
  40. messaging options document-level DOM events super-basic pub/sub promises third-party utilities

    with multiple options
  41. these are all legit. but weG re going to use

    the simplest
  42. we want events to be scoped to objects F r

    m w r k . O b j e c t = f u n c t i o n ( o b j ) { $ . e x t e n d ( t r u e , t h i s , { s e l : " b o d y " , d a t a : { } , e n d p o i n t : n u l l , e v e n t s : { } } , o b j ) ; i f ( t h i s . s e l ) { t h i s . e l = $ ( t h i s . s e l ) ; } r e t u r n t h i s ; } ;
  43. weG ll manage that cache with three methods F r

    m w r k . O b j e c t . p r o t o t y p e . p u b = _ p u b = f u n c t i o n ( n a m e , a r g s ) { v a r t h a t = t h i s ; i f ( t h i s . e v e n t s [ n a m e ] ) { $ . e a c h ( t h i s . e v e n t s [ n a m e ] , f u n c t i o n ( ) { t h i s . a p p l y ( t h a t , a r g s | | [ ] ) ; } ) ; } } ; F r m w r k . O b j e c t . p r o t o t y p e . s u b = _ s u b = f u n c t i o n ( n a m e , c a l l b a c k ) { i f ( ! t h i s . e v e n t s [ n a m e ] ) { t h i s . e v e n t s [ n a m e ] = [ ] ; } t h i s . e v e n t s [ n a m e ] . p u s h ( c a l l b a c k ) ; r e t u r n [ n a m e , c a l l b a c k ] ; } ; F r m w r k . O b j e c t . p r o t o t y p e . u n s u b = _ u n s u b = f u n c t i o n ( h a n d l e ) { v a r e v t s = t h i s . e v e n t s [ h a n d l e [ 0 ] ] , hat tip to Pete Higgins
  44. now when something happens, we can publish an event F

    r m w r k . O b j e c t . p r o t o t y p e . s e t = _ s e t = f u n c t i o n ( d a t a , v a l ) { i f ( t y p e o f d a t a = = " S t r i n g " ) { t h i s . d a t a [ d a t a ] = v a l ; } e l s e { f o r ( k e y i n d a t a ) { t h i s . d a t a [ k e y ] = d a t a [ k e y ] ; } } t h i s . p u b ( " u p d a t e d " , t h i s . d a t a ) ; i f ( t h i s . e n d p o i n t ) { t h i s . s y n c ( ) ; } r e t u r n t h i s ; } ;
  45. or if the wrong thing happens, we can notify observers

    of the error F r m w r k . O b j e c t . p r o t o t y p e . s y n c = f u n c t i o n ( d a t a , c m d ) { v a r i d = t y p e o f d a t a = = " n u m b e r " ? d a t a : u n d e f i n e d , t h a t = t h i s ; $ . p o s t ( t h i s . e n d p o i n t , { i d : d a t a . i d | | i d | | u n d e f i n e d , c o m m a n d : c m d | | u n d e f i n e d , d a t a : i d ? u n d e f i n e d : d a t a } , f u n c t i o n ( r e s ) { i f ( t y p e o f r e s = = " n u m b e r " ) { t h a t . d a t a . i d = r e s ; } e l s e i f ( r e s . i d ) { f o r ( k e y i n r e s ) { t h a t . d a t a [ k e y ] = d a t a [ k e y ] ; } }
  46. global events can be scoped to the app F r

    m w r k . p u b = f u n c t i o n ( ) { _ p u b . a p p l y ( F r m w r k . a p p , a r g u m e n t s ) ; } ; F r m w r k . s u b = f u n c t i o n ( ) { r e t u r n _ s u b . a p p l y ( F r m w r k . a p p , a r g u m e n t s ) ; } ; F r m w r k . u n s u b = f u n c t i o n ( ) { _ u n s u b . a p p l y ( F r m w r k . a p p , a r g u m e n t s ) ; } ;
  47. notifications as an implementer controllers, presenters, viewmodels ideally, publishing events

    controlled by framework implementations merely listen but we have events we don't control coming from the DOM
  48. connecting to the view

  49. a JS framework should be decoupled from HTML and CSS

    maintainability modularity separation of concerns not using JS for things that don't require it
  50. note: this is JS frameworks client-side frameworks may be more

    tightly coupled, and do more view stuff
  51. elements of the view templates CSS DOM listeners potentially a

    data transformation layer
  52. an object might have more than one view, though for

    that we need states
  53. a new object type F r m w r k

    . S t a t e = f u n c t i o n ( o b j ) { $ . e x t e n d ( t r u e , t h i s , { p a r e n t : F r m w r k . a p p , t m p l : n u l l , s e t t i n g s : { } , c a l l b a c k : n u l l } , o b j ) ; r e t u r n t h i s ; } ;
  54. tied to the generic object F r m w r

    k . O b j e c t = f u n c t i o n ( o b j ) { $ . e x t e n d ( t r u e , t h i s , { s e l : " b o d y " , d a t a : { } , e n d p o i n t : n u l l , e v e n t s : { } , s t a t e s : { } } , o b j ) ; i f ( t h i s . s e l ) { t h i s . e l = $ ( t h i s . s e l ) ; } r e t u r n t h i s ; } ;
  55. child objects force us to think about initialization we want

    implementers to be able to pass in literals but we want instances that can have prototype methods our options are: 1. force states to be added explicitly 2. filter states supplied on initialization
  56. this is a place where itG s nice to do

    some magic F r m w r k . O b j e c t = f u n c t i o n ( o b j ) { $ . e x t e n d ( t r u e , t h i s , { s e l : " b o d y " , d a t a : { } , e n d p o i n t : n u l l , e v e n t s : { } , s t a t e s : { } } , o b j ) ; i f ( t h i s . s e l ) { t h i s . e l = $ ( t h i s . s e l ) ; } f o r ( v a r s i n t h i s . s t a t e s ) { i f ( ! t h i s . s t a t e s [ s ] i n s t a n c e o f F r m w r k . S t a t e ) { t h i s . s t a t e s [ s ] = n e w F r m w r k . S t a t e ( t h i s . s t a t e s [ s ] ) ; } }
  57. once we have states we can switch between them F

    r m w r k . O b j e c t . p r o t o t y p e . s t a t e = _ s t a t e = f u n c t i o n ( n a m e ) { i f ( ! n a m e ) { r e t u r n t h i s . _ c u r r e n t S t a t e = = " _ d e f a u l t " ? n u l l : t h i s . _ c u r r e n t S t a t e } i f ( t h i s . s t a t e s [ n a m e ] ) { t h i s . s t a t e s [ n a m e ] . r e n d e r ( ) ; t h i s . _ c u r r e n t S t a t e = n a m e ; r e t u r n t h i s . s t a t e s [ n a m e ] ; } } ; F r m w r k . s t a t e = f u n c t i o n ( ) { _ s t a t e . a p p l y ( F r m w r k . a p p , a r g u m e n t s ) ; } ;
  58. and, since theyG re instances, use their methods bringing us

    back to templates
  59. very simple template rendering F r m w r k

    . S t a t e . p r o t o t y p e . r e n d e r = f u n c t i o n ( ) { v a r d a t a = $ . e x t e n d ( t r u e , { } , t h i s . p a r e n t . d a t a , t h i s . s e t t i n g s ) ; t h i s . p a r e n t . e l . h t m l ( t h i s . t m p l ( d a t a ) ) ; i f ( t h i s . c a l l b a c k ) { t h i s . c a l l b a c k . a p p l y ( t h i s , a r g u m e n t s ) ; } } ;
  60. contains several assumptions: t m p l has already been

    compiled to a function a state will always replace the object it belongs to any partial templates are already accessible somehow the correct CSS is already loaded
  61. thatG s a lot can we make some of those

    more flexible or automatic
  62. easy fixes F r m w r k . S

    t a t e . p r o t o t y p e . r e n d e r = f u n c t i o n ( ) { v a r d a t a = $ . e x t e n d ( t r u e , { } , t h i s . p a r e n t . d a t a , t h i s . s e t t i n g s ) ; i f ( t h i s . c s s ) { $ ( " h e a d " ) . a p p e n d ( ' < s t y l e c l a s s = " ' + t h i s . _ n a m e + ' " > ' + t h i s . c s s + } t h i s . c o n t a i n e r ? t h i s . c o n t a i n e r . h t m l ( t h i s . t m p l ( d a t a ) ) : t h i s . p a r e n t . e l . h t m l ( t h i s . t m p l ( d a t a ) ) ; i f ( t h i s . c a l l b a c k ) { t h i s . c a l l b a c k . a p p l y ( t h i s , a r g u m e n t s ) ; } } ;
  63. loading templates and partials is trickier should be able to

    take a URL or a template element should know whether the template's already loaded should handle partials differently and know which partials it requires
  64. we should handle that in our constructor F r m

    w r k . S t a t e = f u n c t i o n ( o b j ) { $ . e x t e n d ( t r u e , t h i s , { p a r e n t : F r m w r k . a p p , t m p l : n u l l , s e t t i n g s : { } , c a l l b a c k : n u l l } , o b j ) ; / / t m p l n e e d s t o b e c o m p i l e d i f ( t h i s . t m p l & & t y p e o f t h i s . t m p l ! = " f u n c t i o n " ) { t h i s . t m p l = _ c o m p i l e T m p l ( t h i s . t m p l , t h i s . p a r t i a l s ) ; } r e t u r n t h i s ; } ;
  65. and do the loading work, if necessary, in a static

    function v a r _ t m p l C a c h e = { } ; f u n c t i o n _ c o m p i l e T m p l ( t m p l S t r i n g , p a r t i a l s , n a m e ) { v a r t m p l N a m e = n a m e | | t m p l S t r i n g . r e p l a c e ( " \ / " , " _ " ) . r e p l a c e ( " . " , d e f = { } ; i f ( p a r t i a l s & & t y p e o f p a r t i a l s = = " o b j e c t " & & p a r t i a l s . l e n g t h ) { p a r t i a l s . f o r E a c h ( f u n c t i o n ( p ) { d e f [ p . n a m e ] = _ t m p l C a c h e [ p . n a m e ] ; } ) ; } i f ( _ t m p l C a c h e [ t m p l N a m e ] ) { r e t u r n _ t m p l C a c h e [ t m p l N a m e ] ; } e l s e i f ( $ ( " b o d y " ) . f i n d ( t m p l S t r i n g ) . l e n g t h ) { r e t u r n _ t m p l C a c h e [ t m p l N a m e ] = d o T . t e m p l a t e ( $ ( t m p l S t r i n g ) . h t m l } e l s e { $ . g e t ( t m p l S t r i n g , f u n c t i o n ( t m p l ) { _ t m p l C a c h e [ t m p l N a m e ] = d o T . t e m p l a t e ( t m p l , n u l l , d e f ) ;
  66. we also need a way to add partials we can't

    expect partials to be tied to an object or state we need them to be available when the templates using them are compiled
  67. thus, the first method belonging exclusively to the application F

    r m w r k . a p p . r e g i s t e r T m p l = _ r e g i s t e r T m p l = f u n c t i o n ( n a m e , t m p l S t r i n g ) { _ c o m p i l e T m p l ( t m p l S t r i n g , n u l l , n a m e ) ; } ;
  68. why not just use _ c o m p i

    l e T m p l we could, but itG s nice to have a separate public interface
  69. it also helps reflect a different use F r m

    w r k . O b j e c t = f u n c t i o n ( o b j ) { $ . e x t e n d ( t r u e , t h i s , { s e l : " b o d y " , d a t a : { } , e n d p o i n t : n u l l , e v e n t s : { } , s t a t e s : { } , _ c u r r e n t S t a t e : " _ d e f a u l t " , t m p l s : { } } , o b j ) ; i f ( t h i s . s e l ) { t h i s . e l = $ ( t h i s . s e l ) ; } i f ( t h i s . t m p l s ) { f o r ( v a r t i n t h i s . t m p l s ) { _ r e g i s t e r T m p l ( t , t h i s . t m p l s [ t ] ) ; }
  70. real talk: that could be much better everything's easy until

    it's async external resources are likely to require async code this is an ideal place for promises ..which we'd get from yet another external resource
  71. we want a framework to also help manage dependencies what

    we have could work if dependencies are finite if they're generic, we'd be better off with something like Require.js something like that means we have to think of the framework differently
  72. taking Require as an example.. our object types would be

    modules the app would be a specific dependency we could remove any code to load templates or CSS all this code would be required by implementations
  73. one more piece wiring up the DOM

  74. weG ve left a place for some implementation code F

    r m w r k . S t a t e = f u n c t i o n ( o b j ) { $ . e x t e n d ( t r u e , t h i s , { p a r e n t : F r m w r k . a p p , t m p l : n u l l , s e t t i n g s : { } , c a l l b a c k : n u l l } , o b j ) ; / / t m p l n e e d s t o b e c o m p i l e d i f ( t h i s . t m p l & & t y p e o f t h i s . t m p l ! = " f u n c t i o n " ) { t h i s . t m p l = _ c o m p i l e T m p l ( t h i s . t m p l , t h i s . p a r t i a l s ) ; } r e t u r n t h i s ; } ;
  75. some frameworks attempt to contain DOM interaction lists of selectors

    and events callbacks belong to the application or a controller this keeps everything nice and clean but doesn't add a whole lot of functionality
  76. what DOM functionality can a framework add re-rendering attaching DOM

    interactions to state changes forwarding DOM events to framework events
  77. the rest is nobodyG s business but the DOMG s

  78. what we need to trigger a state change an element

    to delegate to the selector and event the name of the state to switch to
  79. we want this wired up when our state is created

    F r m w r k . S t a t e = f u n c t i o n ( o b j ) { $ . e x t e n d ( t r u e , t h i s , { p a r e n t : F r m w r k . a p p , t m p l : n u l l , s e t t i n g s : { } , c a l l b a c k : n u l l , t r i g g e r : n u l l } , o b j ) ; / / t m p l n e e d s t o b e c o m p i l e d i f ( t h i s . t m p l & & t y p e o f t h i s . t m p l ! = " f u n c t i o n " ) { t h i s . t m p l = _ c o m p i l e T m p l ( t h i s . t m p l , t h i s . p a r t i a l s ) ; } i f ( t h i s . t r i g g e r & & t h i s . t r i g g e r . l e n g t h ) { t h i s . t r i g g e r . f o r E a c h ( f u n c t i o n ( t ) { $ ( t . c o n t a i n e r | | t h i s . p a r e n t . e l ) . o n ( t . e v e n t , t . s e l e c t o r , t h i s } ) ; }
  80. how about more generic events we may still want to

    fire events scoped to an object our publish method is public, so implementers can do that manually for something very small and unopinionated, that's probably enough
  81. but we should talk about opinions

  82. this example is more useful as a complement to something

    else lots of control in HTML or CSS a DOM library a datavis library a widget or component framework a client/server framework
  83. there are a lot of tools but only a handful

    of strategies for augmenting them
  84. with HTML/CSS framework provides initial data and rendering states can

    be exclusively data states, not visual state callbacks set up explicit data updates framework exists to store and sync data
  85. CSS specifically cause thatG s kind of hard CSS interactions

    are an alternative to JS we can observe animations (e.g. a n i m a t i o n S t a r t , a n i m a t i o n E n d ) but we can't directly observe something activating : t a r g e t , for example and we can't forward events from CSS to JS the most foolproof way to integrate with CSS is to listen for the same events
  86. with DOM-heavy libraries or plugins object DOM element is just

    the container stores data for child elements that aren't objects child elements created by object's template object's default state callback listens for library events and updates its data accordingly again, storing and syncing
  87. with components or widgets framework objects could be made less

    generic to be aware of library functions or the two can communicate from their controller code components should manage themselves, so framework only needs to worry about public notifications still storing and syncing, but at a higher level of remove
  88. with frameworks that handle the backend again, we can be

    less generic and rely on server- aware objects those may still need rendering and client-only messaging this time, we're observing third-party events coming not from the user but the server or the data handle the manipulation of data, leave the rest to the underlying framework
  89. in the real world, we usually only think about pieces

    of frameworks in conjunction with existing frameworks
  90. what to do with these ideas don't go write a

    new generic do-everything framework think about what pieces your app needs from a framework choose frameworks that are opionated about those things if you can't find a perfect fit, write a small wrapper that adds just what you need
  91. thanks! any questions | garann.com @garannm